package httptool

import (
	"bytes"
	"context"
	"fmt"
	"io"
	"net/http"
	"time"

	"go-mall/common/errcode"
	"go-mall/common/logger"
	"go-mall/common/util"
)

func Request(method string, url string,
	options ...Option,
) (httpStatusCode int, respBody []byte, err error) {
	start := time.Now()
	reqOpts := defaultRequestOptions()

	for _, opt := range options {
		err = opt.apply(reqOpts)
		if err != nil {
			return
		}
	}
	log := logger.New(reqOpts.ctx)
	defer func() {
		if err != nil {
			log.Error("HTTP_REQUEST_ERROR_LOG",
				"method", method,
				"url", url,
				"data", reqOpts.data,
				"reply", respBody,
				"err", err,
			)
		}
	}()

	req, err := http.NewRequest(method, url, bytes.NewReader(reqOpts.data))
	if err != nil {
		return
	}

	req = req.WithContext(reqOpts.ctx)
	defer req.Body.Close()

	traceId, spanId, _ := util.GetTraceInfoFromCtx(reqOpts.ctx)
	reqOpts.headers["traceid"] = traceId
	reqOpts.headers["spanid"] = spanId
	if len(reqOpts.headers) != 0 {
		for k, v := range reqOpts.headers {
			req.Header.Set(k, v)
		}
	}

	client := &http.Client{Timeout: reqOpts.timeout}
	resp, err := client.Do(req)
	if err != nil {
		return
	}
	defer resp.Body.Close()

	dur := time.Since(start).Milliseconds()
	if dur >= 3000 {
		log.Warn("HTTP_REQUEST_SLOW_LOG",
			"method", method,
			"url", url,
			"data", reqOpts.data,
			"reply", respBody,
			"err", err,
			"dur(ms)", dur,
		)
	}
	log.Debug("HTTP_REQUEST_DEBUG_LOG",
		"method", method,
		"url", url,
		"data", reqOpts.data,
		"reply", respBody,
		"err", err,
		"dur(ms)", dur,
	)

	httpStatusCode = resp.StatusCode
	if httpStatusCode != http.StatusOK {
		errcode.Wrap("request api error",
			fmt.Errorf("non 200 status code: %d", httpStatusCode))
		return
	}

	respBody, _ = io.ReadAll(resp.Body)

	return httpStatusCode, respBody, err
}

type (
	requestOption struct {
		ctx     context.Context
		timeout time.Duration
		data    []byte
		headers map[string]string
	}
	Option interface {
		apply(option *requestOption) error
	}
)

func defaultRequestOptions() *requestOption {
	return &requestOption{
		ctx:     context.Background(),
		timeout: 5 * time.Second,
		data:    nil,
		headers: map[string]string{},
	}
}

func Get(ctx context.Context, url string,
	options ...Option,
) (httpStatusCode int, respBody []byte, err error) {
	options = append(options, WithContext(ctx))
	return Request(http.MethodGet, url, options...)
}

func Post(ctx context.Context, url string,
	data []byte, options ...Option,
) (httpStatusCode int, respBody []byte, err error) {
	defaultHeader := map[string]string{"Content-Type": "application/json"}
	var newOptions []Option
	newOptions = append(newOptions,
		WithHeader(defaultHeader),
		WithData(data),
		WithContext(ctx),
	)
	newOptions = append(newOptions, options...)
	return Request(http.MethodPost, url, newOptions...)
}

func WithContext(ctx context.Context) Option {
	return optionFunc(func(option *requestOption) (err error) {
		option.ctx = ctx
		return
	})
}

type optionFunc func(option *requestOption) error

func (f optionFunc) apply(opts *requestOption) error {
	return f(opts)
}

func WithHeader(headers map[string]string) Option {
	return optionFunc(func(option *requestOption) (err error) {
		for k, v := range headers {
			option.headers[k] = v
		}
		return
	})
}

func WithData(data []byte) Option {
	return optionFunc(func(option *requestOption) (err error) {
		option.data = data
		return
	})
}

func WithTimeout(timeout time.Duration) Option {
	return optionFunc(func(option *requestOption) error {
		option.timeout = timeout
		return nil
	})
}
